using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.Reflection;


namespace UKVSTS.WebTestPlugins
{
    public class ScriptableParameterPlugIn : WebTestPlugin
    {
        private const String StartTag = "<%";

        private const String EndTag = "%>";

        /// <summary>
        /// We are going to be caching any compiled up scripts, so we need somewhere to store them.
        /// </summary>
        private static Dictionary<String, ScriptedParamDetails> ms_ScriptCache = new Dictionary<string, ScriptedParamDetails>();

        public override void PostWebTest(object sender, PostWebTestEventArgs e)
        {

        }

        /// <summary>
        /// Initialises and wires up our event handler
        /// </summary>
        public override void PreWebTest(object sender, PreWebTestEventArgs e)
        {
            e.WebTest.PreRequest += new EventHandler<PreRequestEventArgs>(WebTest_PreRequest);

            // Check all the Context Parameters for any scripts
            //
            String[] keys = new String[e.WebTest.Context.Keys.Count];
            e.WebTest.Context.Keys.CopyTo(keys, 0);

            foreach (String key in keys)
            {
                // Not all items in the context will be a string, see:
                //   http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=3006040&SiteID=1
                // for mor information.
                //
                String value = e.WebTest.Context[key] as String;
                if (value != null) {
                    e.WebTest.Context[key] = ModifyIfScriptPresent(key, value, null);
                }
            }
        }

        void WebTest_PreRequest(object sender, PreRequestEventArgs e)
        {
            // We are going to need to check all params on all requests to see if they contain
            // a script block.
            //
            try
            {
                // Query string first
                //
                foreach (Parameter param in e.Request.QueryStringParameters)
                {
                    param.Value = ModifyIfScriptPresent(param.Name, param.Value, e);
                }

                // Now the form post params - if there are any.
                //
                FormPostHttpBody formBody = e.Request.Body as FormPostHttpBody;
                if (formBody != null)
                {
                    foreach (Parameter p in formBody.FormPostParameters)
                    {
                        // If this is a file upload param, lets check to see if we 
                        // have some script to determin the file that gets loaded.
                        //
                        FileUploadParameter fileP = p as FileUploadParameter;
                        if (fileP != null)
                        {
                            fileP.FileName = ModifyIfScriptPresent(p.Name, fileP.FileName, e);
                        }
                        else
                        {
                            p.Value = ModifyIfScriptPresent(p.Name, p.Value, e);
                        }
                    }
                }

                // Is this a "web service" call?
                StringHttpBody httpBody = e.Request.Body as StringHttpBody;
                if (httpBody != null)
                {
                    httpBody.BodyString = ModifyIfScriptPresent("XML body", httpBody.BodyString, e);
                }

            }
            catch (Exception ex)
            {
                SetError(e, ex);
            }
        }

        /// <summary>
        /// Sets the context for this request to indicate that an error has occured
        /// This funcyion also subscribes the Request_ValidateResponse method to the
        /// request's ValidateResponse event. This then displays, to the UI, that an
        /// error has occured.
        /// </summary>
        /// <param name="e"></param>
        /// <param name="ex"></param>
        private void SetError(PreRequestEventArgs e, Exception ex)
        {
            e.WebTest.Context["UKVSTS.WebTestPlugin.Error"] = ex;
            e.Request.ValidateResponse += new EventHandler<ValidationEventArgs>(Request_ValidateResponse);
        }

        /// <summary>
        /// Returns the exception that is associated with the passed test.
        /// If no error has occured, null is returned.
        /// </summary>
        /// <param name="e"></param>
        /// <returns></returns>
        private Exception GetError(WebTest test)
        {
            Object ex = null;
            test.Context.TryGetValue("UKVSTS.WebTestPlugin.Error", out ex);
            return (Exception)ex;
        }

        /// <summary>
        /// Indicates to the UI that an error has occured with this request.
        /// </summary>
        void Request_ValidateResponse(object sender, ValidationEventArgs e)
        {
            Exception ex = GetError(e.WebTest);

            if (ex != null)
            {
                String paramName = "Unknown";
                ScriptedParamException spx = ex as ScriptedParamException;
                if (spx != null)
                {
                    paramName = spx.ParamName;
                }

                // This doen't give a fantatsic experience to the user, but does at lease show that
                // something went wrong.
                e.IsValid = false;
                e.Message = String.Format("Error with scripted parameter \"{0}\": {1}", paramName, ex.Message);
            }
        }

        /// <summary>
        /// Checks the passed parameter to see if it contains a script block. If it does, then the script
        /// will be evaluated and the parameter modified to reflect the value returned from the script.
        /// </summary>
        private String ModifyIfScriptPresent(String name, String value, PreRequestEventArgs e)
        {
            int startFrom = 0;
            int startTagAt = 0;
            int endTagAt = 0;

            int scriptNo = 0;

            try
            {
                String paramValue = value;

                // We are going to build up any modified value in this buffer
                //
                StringBuilder sb = new StringBuilder();

                // Start looking for script blocks. There may be more than one block in
                // a parameter, so keep looking after we fing a "%>"
                //
                while ((startTagAt = paramValue.IndexOf(StartTag, startFrom)) != -1)
                {
                    // copy the literal string preceding the "<%" to the buffer.
                    //
                    sb.Append(paramValue, startFrom, startTagAt - startFrom);


                    endTagAt = paramValue.IndexOf(EndTag, startTagAt);

                    // Check that the end tag exists
                    //
                    if (endTagAt == -1)
                    {
                        // got a problem ...
                        //
                        throw new ScriptedParamException("Missing end script tag in parameter value", name);
                    }

                    // Extract the script from the <% %>
                    //
                    String code = paramValue.Substring(startTagAt + StartTag.Length,
                                                       (endTagAt - 1) - (startTagAt + StartTag.Length));

                    // Have we already compiled this script?
                    //
                    ScriptedParamDetails sd = null;
                    if (ms_ScriptCache.TryGetValue(code, out sd) == false)
                    {
                        // Probably not, buts need to lock and "double check"
                        //
                        lock (ms_ScriptCache)
                        {
                            if (ms_ScriptCache.TryGetValue(code, out sd) == false)
                            {
                                // Ok, this is the first time we have seen this script, so need to compile it up.
                                //
                                sd = new ScriptedParamDetails(name, code);

                                // And cache it
                                //
                                ms_ScriptCache[code] = sd;
                            }

                        }
                    }

                    // Execute the script and capture the value it returns.
                    //
                    String scriptResult = sd.InvokeScript(e);
                    sb.Append(scriptResult);

                    if (e != null && e.WebTest.Context != null)
                    {
                        // Add the param to the test context, so we could use the value in
                        // an extraction / validation rule or subsiquesnt request if we need to.

                        String contextIndex = name;

                        // If this isn't the first script block for this param, then add on 
                        // the scriptNo to the param name to create a (probably?) unique name.
                        if (scriptNo != 0)
                        {
                            contextIndex = name + "_" + scriptNo.ToString();
                        }
                        e.WebTest.Context[contextIndex] = scriptResult;
                        scriptNo++;
                    }

                    startFrom = endTagAt + EndTag.Length;
                }

                // Copy the remaining part of the string across.
                // If there are no script tags in this param value, then startFrom will be 0,
                // so we can just exit without making any changes.
                //
                if (startFrom != 0)
                {
                    sb.Append(paramValue, startFrom, paramValue.Length - startFrom);
                    value = sb.ToString();
                }
            }
            catch (Exception)
            {
                value = "";
                throw;
            }

            return value;
        }
    }
}
